package dijjer.updater;

import dijjer.util.*;
import dijjer.util.logging.*;

import java.io.*;
import java.net.*;
import java.util.*;

public class ResourceManager implements NonUpdateable {

	public static final String VERSION = "$Id: ResourceManager.java 2 2008-03-21 14:33:03Z ian.clarke $";
	public static InetAddress mcastAddr;
	public static final int mcastPort = 9113;

	private static final Map _UDPSockets = Collections.synchronizedMap(new HashMap());
	private static final Map _MCSockets = Collections.synchronizedMap(new HashMap());	
	private static final Map _TCPSockets = Collections.synchronizedMap(new HashMap());

	private static String _downloadURL = "http://downloads.dijjer.org/dijjer.jar";
	private static File _classCacheDirectory;
	private static File _dataDirectory;
	private static boolean _shutdown = false;

    private static final List _validAddresses = new ArrayList();

    static {
		try {
			mcastAddr = InetAddress.getByName("239.250.240.230");
		} catch (UnknownHostException e) {
			mcastAddr = null;
		}
    }
    
    public static void addValidAddress(String address)
    {
        _validAddresses.add(address);
    }

    public static boolean validAddress(InetAddress addr)
    {
        if (addr.isAnyLocalAddress() || addr.isLoopbackAddress()) {
            return true;
        }
        return _validAddresses.contains(addr.getHostName())
            || _validAddresses.contains(addr.getHostAddress());
    }

	public static File downloadCodeBase() throws Exception {
		File jar = new File(_classCacheDirectory, System.currentTimeMillis() + ".dijjer.jar");
		Logger.debug("Downloading from " + _downloadURL + " to " + jar);

		BufferedInputStream in = new BufferedInputStream(new URL(_downloadURL).openConnection().getInputStream());
		FileOutputStream out = new FileOutputStream(jar, false);

		byte[] bytes = new byte[1024];
		int c = -1;
		while ((c = in.read(bytes)) != -1) {
			out.write(bytes, 0, c);
		}

		in.close();
		out.close();

		return jar;
	}

	public static void setDownloadURL(String downloadURL) {
		_downloadURL = downloadURL;
	}

	public static File getRuntimeBase() throws Exception {
		if (_classCacheDirectory == null) {
			return null;
		}
		File[] files = _classCacheDirectory.listFiles(new FilenameFilter() {
			public boolean accept(File dir, String name) {
				return name.endsWith(".jar");
			}
		});
		if (files.length == 0) {
			return downloadCodeBase();
		}
		File recent = null;

		for (int i = 0; i < files.length; i++) {
			File file = files[i];
			if (recent == null || file.lastModified() > recent.lastModified()) {
				recent = file;
			}
		}
		return recent;
	}


	public static void cleanupCache() {
	}

	public static void setClassCacheDirectory(File classCacheDirectory) {
		Logger.init("Class cache:" + classCacheDirectory);
		_classCacheDirectory = classCacheDirectory;
	}

	public static void setDataDirectory(File dataDirectory) {
		Logger.init("Data cache:" + dataDirectory);
		_dataDirectory = dataDirectory;
	}

	public static File getDataDirectory() {
		return _dataDirectory;
	}

	public static String getDownloadURL() {
		return _downloadURL;
	}

	public static void createUDPSocket(int port) {
		checkShutdown();
		synchronized (_UDPSockets) {
			if (!_UDPSockets.containsKey(new Integer(port))) {
				try {
					_UDPSockets.put(new Integer(port), new UDPSocket(port) {
							{
							startThread();
							}
							});
				} catch (Exception e) {
					Logger.fatal(-1, "Couldn't connect to UDP port " + port + 
					                 ", is another instance of running on the same port?");
				}
			}
		}
	}
	
	public static void createMCSocket(int port) {
		checkShutdown();
		synchronized (_MCSockets) {
			if (!_MCSockets.containsKey(new Integer(port))) {
				try {
					_MCSockets.put(new Integer(port), new MCSocket(port) {
						{
							startThread();
						}
					});
				} catch (Exception e) {
					Logger.debug("Couldn't connect to MC port " + port + ", is another instance of running on the same port?", e);
				}
			}
		}
	}

	private static void createTCPSocket(int port) {
		checkShutdown();
		synchronized (_TCPSockets) {
			if (!_TCPSockets.containsKey(new Integer(port))) {
				try {
					_TCPSockets.put(new Integer(port), new TCPSocket(port) {
							{
							startThread();
							}
							});
				} catch (Exception e) {
					Logger.fatal(-1, "Couldn't connect to TCP port " + port + 
					                 ", is another instance of running on the same port?");
				}
			}
		}
	}

	public static DatagramPacket receive(int port) {
		createUDPSocket(port);
		UDPSocket socket = (UDPSocket) _UDPSockets.get(new Integer(port));
		return socket.receive();
	}
	
	public static DatagramPacket receiveMC(int port) {
		createMCSocket(port);
		MCSocket socket = (MCSocket) _MCSockets.get(new Integer(port));
		return socket.receive();
	}

	public static void send(int port, DatagramPacket packet) {
		if (packet.getAddress().equals(mcastAddr)) {
			createMCSocket(mcastPort);
			MCSocket socket = (MCSocket) _MCSockets.get(new Integer(mcastPort));			
			socket.send(packet);
		} else {
			createUDPSocket(port);
			UDPSocket socket = (UDPSocket) _UDPSockets.get(new Integer(port));
			socket.send(packet);
		}
	}

	public static String getUDPSocketString(int port) {
		createUDPSocket(port);
		UDPSocket socket = (UDPSocket) _UDPSockets.get(new Integer(port));
		if (socket != null) {
		   return socket.toString();
		}
		return "UNBOUND";
	}
	
	public static String getMCSocketString(int port) {
		createMCSocket(port);
		MCSocket socket = (MCSocket) _MCSockets.get(new Integer(port));
		if (socket != null) {
		   return socket.toString();
		}
		return "UNBOUND";
	}

	public static Socket accept(int port) {
		createTCPSocket(port);
		TCPSocket socket = (TCPSocket) _TCPSockets.get(new Integer(port));
		return socket.accept();
	}

	public static void softShutdown() {
		_shutdown = true;
	}

	public static void shutdown() {
		_shutdown = true;
		for (Iterator i = _TCPSockets.values().iterator(); i.hasNext();) {
			TCPSocket tcpSocket = (TCPSocket) i.next();
			tcpSocket.shutdownThread();
		}
		for (Iterator i = _UDPSockets.values().iterator(); i.hasNext();) {
			UDPSocket udpSocket = (UDPSocket) i.next();
			udpSocket.shutdownThread();
		}
		for (Iterator i = _MCSockets.values().iterator(); i.hasNext();) {
			MCSocket multicastSocket = (MCSocket) i.next();
			multicastSocket.shutdownThread();
		}
	}

	private static void checkShutdown() {
		if (_shutdown) {
			throw new RuntimeException("Resource manager is shut down!");
		}
	}

	public static void revive() {
		_shutdown = false;
	}

	private static class UDPSocket extends AbstractThread {

		private final DatagramSocket _socket;
		private final List _packets = new ArrayList(15);

		public UDPSocket(int port) throws Exception {
			long wait = 2;
			DatagramSocket socket = null;
			while (socket == null) {
				try {
					socket = new DatagramSocket(port);
				} catch (SocketException e) {
					Logger.warning("Could not bind to socket on port: " + port + ", trying again in " + wait + " seconds.");
					Thread.sleep(wait * 1000);
					wait *= 2;
				}
			}
			_socket = socket;
		}

		public DatagramPacket receive() {
			synchronized (_packets) {
				if (_packets.isEmpty() && !wasShutdown()) {
					checkShutdown();
					try {
						_packets.wait(1000);
					} catch (InterruptedException ex) {
						return null;
					}
				}
				if (_packets.isEmpty()) {
					return null;
				}
				return (DatagramPacket) _packets.remove(0);
			}
		}

		protected boolean loop() throws InterruptedException {
			DatagramPacket packet = new DatagramPacket(new byte[1500], 1500);
			try {
				_socket.receive(packet);
				synchronized (_packets) {
					_packets.add(packet);
					_packets.notifyAll();
				}

			} catch (SocketException e) {
				if (wasKilled() || wasShutdown()) {
					Logger.info("Shutting down UDP socket");
				} else {
					Logger.error("Problem receiving UDP request!", e);
				}
			} catch (IOException e) {
				Logger.error("Problem receiving UDP packet!", e);
			}
			return true;
		}

		public void send(DatagramPacket packet) {
			try {
				_socket.send(packet);
			} catch (Exception e) {
				Logger.error("Error while sending packet to " + packet.getSocketAddress(), e);
			}
		}

		protected void cleanUp() {
			if (!_packets.isEmpty()) {
				Logger.warning("Dumping "+ _packets.size() +" unused packets.");
			}
			_packets.clear();
		}

		protected void shutdown() {
			try {
				_socket.close();
			} catch (Exception expected) {
				Logger.debug("Junk exception: ", expected);
			}
		}

		public String toString() {
			return _socket.getLocalAddress() + ":" + _socket.getLocalPort();
		}
	}

	private static class MCSocket extends AbstractThread {

		private final MulticastSocket _socket;
		private final List _packets = new ArrayList(15);

		public MCSocket(int port) throws Exception {
			long wait = 2;
			MulticastSocket socket = null;
			while (socket == null) {
				try {
					socket = new MulticastSocket(port);
					socket.joinGroup(mcastAddr);
					socket.setTimeToLive(1);
				} catch (SocketException e) {
					Logger.warning("Could not bind to socket on port: " + port + ", trying again in " + wait + " seconds.");
					Thread.sleep(wait * 1000);
					wait *= 2;
				}
			}
			_socket = socket;
		}

		public DatagramPacket receive() {
			synchronized (_packets) {
				if (_packets.isEmpty() && !wasShutdown()) {
					checkShutdown();
					try {
						_packets.wait(1000);
					} catch (InterruptedException ex) {
						return null;
					}
				}
				if (_packets.isEmpty()) {
					return null;
				}
				return (DatagramPacket) _packets.remove(0);
			}
		}

		protected boolean loop() throws InterruptedException {
			DatagramPacket packet = new DatagramPacket(new byte[1500], 1500);
			try {
				_socket.receive(packet);
				synchronized (_packets) {
					_packets.add(packet);
					_packets.notifyAll();
				}

			} catch (SocketException e) {
				if (wasKilled() || wasShutdown()) {
					Logger.info("Shutting down MC socket");
				} else {
					Logger.error("Problem receiving MC request!", e);
				}
			} catch (IOException e) {
				Logger.error("Problem receiving MC packet!", e);
			}
			return true;
		}

		public void send(DatagramPacket packet) {
			try {
				_socket.send(packet);
			} catch (Exception e) {
				Logger.error("Error while sending packet to " + packet.getSocketAddress(), e);
			}
		}

		protected void cleanUp() {
			if (!_packets.isEmpty()) {
				Logger.warning("Dumping "+ _packets.size() +" unused packets.");
			}
			_packets.clear();
		}

		protected void shutdown() {
			try {
				_socket.leaveGroup(ResourceManager.mcastAddr);
			} catch (IOException e) {
				Logger.debug("Problem leaving multicast group: ", e);
			}
			
			try {
				_socket.close();
			} catch (Exception expected) {
				Logger.debug("Junk exception: ", expected);
			}
		}

		public String toString() {
			return _socket.getLocalAddress() + ":" + _socket.getLocalPort();
		}
	}

	private static class TCPSocket extends AbstractThread {

		private final ServerSocket _socket;
		private final List _sockets = new ArrayList(15);

		public TCPSocket(int port) throws Exception {
			long wait = 2;
			ServerSocket socket = null;
			while (socket == null) {
				try {
					socket = new ServerSocket(port);
				} catch (IOException e) {
					if (wait == 2) {
						Logger.info("Attempting to shutdown a previous dijjer on port: " + port);
						URL shutdown = new URL("http://127.0.0.1:" + port + "/shutdown?confirm=yes");
						shutdown.getContent();
					}
					
					Logger.warning("Could not bind to socket on port: " + port + ", trying again in " + wait + " seconds.");
					Thread.sleep(wait * 1000);
					wait *= wait;
				}
			}
			_socket = socket;
		}

		public Socket accept() {
			synchronized (_sockets) {
				while (_sockets.isEmpty() && !wasShutdown() && !wasShutdown()) {
					checkShutdown();
					try {
						_sockets.wait(2000);
					} catch (InterruptedException ex) {
						return null;
					}
				}
				return (Socket) _sockets.remove(0);
			}
		}

		protected boolean loop() throws InterruptedException {
			try {
				Socket socket = _socket.accept();
				synchronized (_sockets) {
					_sockets.add(socket);
					_sockets.notifyAll();
				}
			} catch (SocketException e) {
				if (wasKilled() || wasShutdown()) {
					Logger.info("Shutting down TCP socket");
				} else {
					Logger.error("Problem receiving TCP request!", e);
				}
			} catch (IOException e) {
				Logger.error("Problem receiving TCP request!", e);
			}
			return true;
		}

		protected void cleanUp() {
			if (!_sockets.isEmpty()) {
				Logger.warning("Dumping " + _sockets.size() + " unused packets.");
			}
			_sockets.clear();
		}

		protected void shutdown() {
			try {
				_socket.close();
			} catch (Exception expected) {
				Logger.debug("Junk exception: ", expected);
			}
		}
	}
}
